package scoping.blocks;

import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import scoping.ScopeTree;
import scoping.helpers.FaultyType.FaultType;
import scoping.helpers.ScopeException;
import ic.ast.decl.DeclClass;
import ic.ast.decl.DeclField;
import ic.ast.decl.DeclMethod;
import ic.ast.decl.DeclVirtualMethod;
import ic.ast.decl.Parameter;
import ic.ast.decl.PrimitiveType;
import ic.ast.decl.Type;
import ic.ast.decl.PrimitiveType.DataType;

public class ClassScope extends ScopeBlock {
	private DeclClass _classDeclaration;
	private HashMap<String, DeclMethod> _methods;
	private HashMap<String, DeclClass> _derivingClasses;
	private HashMap<String, Integer> _fieldOffsets;
	private HashMap<DeclMethod, Integer> _methodOffsets;
	private LinkedHashMap<DeclMethod, DeclClass> _methodsToClass;
	
	public ClassScope(DeclClass classDeclaration , boolean isHandleMain) throws ScopeException{
		_classDeclaration = classDeclaration;
		_methods = new HashMap<>();
		_fieldOffsets = new HashMap<>();
		_methodOffsets = new HashMap<>();
		_methodsToClass = new LinkedHashMap<>();
		_derivingClasses = new HashMap<>();
		populateVariables(_classDeclaration.getFields(), _classDeclaration.getMethods(), isHandleMain);
	}
	
	public void init(){
		if (!_classDeclaration.getName().equals("Library")){
			populateOffsets(_classDeclaration.getFields(), _classDeclaration.getMethods());
		}
		
	}
	
	public DeclClass getClassDeclaration(){
		return _classDeclaration;
	}
	
	public boolean containsMethod(String methodName){
		return _methods.containsKey(methodName);
	}
	
	public DeclMethod getMethod(String methodName){
		if (_methods.containsKey(methodName))
		{
			return _methods.get(methodName);
		}
		ClassScope parentClassScope = getParentClassScope();
		if (parentClassScope != null){
			return parentClassScope.getMethod(methodName);
		}
		return null;
	}
	
	public Collection<DeclMethod> getMethods(){
		return _methods.values();
	}
	
	public boolean hasDerivingClasses(){
		return _derivingClasses.size() > 0;
	}
	
	public Collection<DeclClass> getDerivingClasses(){
		return _derivingClasses.values();
	}
	
	public DeclClass getDerivingClass(String className){
		return _derivingClasses.get(className);
	}
	
	public void addDerivingClass(DeclClass derivingClass){
		_derivingClasses.put(derivingClass.getName(), derivingClass);
	}
	
	private void populateVariables (List<DeclField> fields, List<DeclMethod> methods , boolean isHandleMain) throws ScopeException /*throws Exception*/
	{
		for (DeclField field : fields)
		{
			if (! TestAndSet(_variables, field.getName(), field.getType()))
			{
				throw new ScopeException(field.getLine(),String.format(FaultType.DEFINED.getDescription(), field.getName()));
			}
		}

		//first level of testing if method is shadowing
		for (DeclMethod method : methods)
		{
			if (isHandleMain)
			{
				handleMain(method);
			}
			methodTestAndSet(_variables, method);
		}
	}
	
	private void populateOffsets (List<DeclField> fields, List<DeclMethod> methods){
		int baseFieldOffset=1;
		int baseMethodOffset=0;
		ClassScope parentClassScope = getParentClassScope();
		if (parentClassScope != null){
			// we don't have to -1 the result since it 
			// already includes the parent DV
			baseFieldOffset = parentClassScope.getClassSize();
			baseMethodOffset = parentClassScope.getMethodsMaxOffset();
		}
		
		for (DeclField field:fields){
			_fieldOffsets.put(field.getName(), baseFieldOffset++);
		}
		
		for (DeclMethod method:methods){
			if (method instanceof DeclVirtualMethod){
				if (parentClassScope != null && parentClassScope instanceof ClassScope &&
						parentClassScope.containsMethod(method.getName())){
					continue;
				}
				_methodOffsets.put(method, baseMethodOffset++);
			}
		}
		setMethodsAndClasses();
	}
	
	private void handleMain(DeclMethod method) throws ScopeException
	{
		if (method.getName().equalsIgnoreCase("main"))
		{
			if (GlobalScope.hasMain()){
				throw new ScopeException(method.getLine(),FaultType.MAIN.getDescription());
			}
			
			if (! (method.getType() instanceof PrimitiveType) || 
					 ! ((PrimitiveType)method.getType()).getDisplayName().equals(DataType.VOID.toString()) 
					|| ((PrimitiveType)method.getType()).getArrayDimension() != 0)
			{
				throw new ScopeException(method.getLine(),FaultType.MAIN_TYPE.getDescription());
			}
			
			if ((method.getFormals().size() != 1))
			{
				throw new ScopeException(method.getLine(),FaultType.MAIN_ARGS.getDescription());
			}
			Parameter parm = method.getFormals().get(0);
			if (!parm.getName().equalsIgnoreCase("args") ||  
					!(parm.getType() instanceof PrimitiveType) ||
					! ((PrimitiveType)parm.getType()).getDisplayName().equals(DataType.STRING.toString()) ||
					((PrimitiveType)parm.getType()).getArrayDimension() != 1)
			{
				throw new ScopeException(method.getLine(),FaultType.MAIN_ARGS.getDescription());
			}
			
			GlobalScope.setHasMain();
		}
	}
	
	public boolean methodTestAndSet (Map<String, Type> map, DeclMethod method) throws ScopeException
	{
		if (! TestAndSet(map, method.getName(), method.getType()))
		{
			if (_methods.containsKey(method.getName())) // method already declared
			{
				throw new ScopeException(method.getLine(), String.format(FaultType.DEFINED.getDescription(), method.getName()));
			}else //method is shadowing a field
			{
				throw new ScopeException(method.getLine(), String.format(FaultType.METHOD_SHADOWING.getDescription(), method.getName()));
			}
		}
		_methods.put(method.getName(), method);
		return true;
	}

	private ClassScope getParentClassScope(){
		ScopeTree parent = _classDeclaration.getScope().getParent();
		if (parent != null && parent.getBlock() instanceof ClassScope){
			return (ClassScope)parent.getBlock();
		}
		return null;
	}
	
	public int getFieldOffset(String fieldName){
		if (_fieldOffsets.containsKey(fieldName))
		{
			return _fieldOffsets.get(fieldName);
		}
		ClassScope parentClassScope = getParentClassScope();
		if (parentClassScope != null){
			return parentClassScope.getFieldOffset(fieldName);
		}
		return -1;
	}
	
	public int getClassSize(){
		int base = 1;
		ClassScope parentClassScope = getParentClassScope();
		if (parentClassScope != null){
			base = parentClassScope.getClassSize();
		}
		return base + _fieldOffsets.size();
	}
	
	public int getMethodOffset(String methodName){
		if (_methods.containsKey(methodName) &&  _methodOffsets.containsKey(_methods.get(methodName)))
		{
			return _methodOffsets.get(_methods.get(methodName));
		}
		ClassScope parentClassScope = getParentClassScope();
		if (parentClassScope != null){
			return parentClassScope.getMethodOffset(methodName);
		}
		return -1;
	}
	
	private int getMethodsMaxOffset(){
		int base = 0;
		ClassScope parentClassScope = getParentClassScope();
		if (parentClassScope != null){
			base = parentClassScope.getMethodsMaxOffset();
		}
		return base + _methodOffsets.size();
	}
		
	private void setMethodsAndClasses()
	{
		ClassScope parentClassScope = getParentClassScope();
		if (parentClassScope != null){
			_methodsToClass = parentClassScope.getMethodsAndClasses();
			for (DeclMethod baseMethod:_methodsToClass.keySet()){
				if (containsMethod(baseMethod.getName())){
					// Override base class if exists in deriving class
					_methodsToClass.put(baseMethod, _classDeclaration);
				}
			}
		}
		
		OffsetComparator offsetComparator = new OffsetComparator(_methodOffsets);
		TreeMap<DeclMethod, Integer> sortedMethodOffsets = new TreeMap<>(offsetComparator);
		sortedMethodOffsets.putAll(_methodOffsets);
		for (Map.Entry<DeclMethod, Integer> entry:sortedMethodOffsets.entrySet()){
			_methodsToClass.put(entry.getKey(), _classDeclaration);
		}
	}
	
	public LinkedHashMap<DeclMethod, DeclClass> getMethodsAndClasses()
	{
		return new LinkedHashMap<>(_methodsToClass);
	}
	
	class OffsetComparator implements Comparator<DeclMethod> {

	    Map<DeclMethod, Integer> base;
	    public OffsetComparator(Map<DeclMethod, Integer> base) {
	        this.base = base;
	    }

	    public int compare(DeclMethod a, DeclMethod b) {
	        return base.get(a) - base.get(b);
	    }
	}
}
